package nl.scalasim
package controlactors

import scala.actors.Actor
import scala.collection.mutable.HashMap
import nl.scalasim.actors.Citizen
import nl.scalasim.actors.traits.{ComposedActor, StoppableActor, SimulationActor}

/**
 * The Choreographer component keeps track of positions and movement
 * of citizens and structures. 
 *
 * @author Yigal Duppen
 * @since Oct 22, 2010
 */
class Choreographer(val map: TileMap) extends ComposedActor
        with StoppableActor
        with Logging
{

  private val structureMap = HashMap[SimulationActor, Position]()
  private val citizenMap = HashMap[Citizen, Position]()
  private val citizenMovement = HashMap[Citizen, Path]()


  def handleAct: PartialFunction[Any, Unit] = {
    case Act => {
      updateMovement
      reply(Ack)
    }
  }
  callbacks :+= handleAct


  def handleActorAdded: PartialFunction[Any, Unit] = {
    case ActorAdded(actor, position) => {
      if (actor.isInstanceOf[Citizen]) {
        addCitizen(actor.asInstanceOf[Citizen], position)
      }
      else {
        addStructure(actor, position)
      }
      reply(Ack)
    }
  }
  callbacks :+= handleActorAdded


  def handleMoveTo: PartialFunction[Any, Unit] = {
    case MoveTo(c: Citizen, target: Actor) => {
      moveCitizen(c, target)
    }
  }
  callbacks :+= handleMoveTo


  def handleFind: PartialFunction[Any, Unit] = {
    case FindClosest(a, f) => {
      log.info("Actor" + a + " needs to find another actor matching " + f)
      a ! Found(findClosest(a, f))
    }
  }
  callbacks :+= handleFind

  def handleCanAdd: PartialFunction[Any, Unit] = {
    case CanAdd(a, p) => {
      reply(canAdd(a, p))
    }
  }
  callbacks :+= handleCanAdd


  private def free(positions: Seq[Position]): Boolean = {
    positions map {
      p =>
        val tile = map(p)
        tile != None && tile.get.structure == None && tile.get.terrain == Normal
    } reduceRight {_ && _}
  }

  /**
   * Checks if a structure can be added
   */
  private def canAdd(d: ActorDescriptor, pos: Position): Boolean = {
    val positions = for (x <- pos._1 to pos._1 + d.width - 1; y <- pos._2 to pos._2 + d.depth - 1) yield (x, y)
    free(positions)
  }

  /**
   * Adds a structure at the defined position.
   */
  private def addStructure(a: SimulationActor, pos: Position) {
    val d = a.descriptor
    if (canAdd(d, pos)) {
      val positions = for (x <- pos._1 to pos._1 + d.width - 1; y <- pos._2 to pos._2 + d.depth - 1) yield (x, y)
      positions foreach {p => map(p).get.structure = Some(a)}

      structureMap(a) = pos
    }
  }

  /**
   * Adds a citizen at the defined position.
   */
  private def addCitizen(c: Citizen, pos: Position) {
    citizenMap(c) = pos
  }

  /**
   * Is there a citizen at the defined position?
   * // todo: use messaging
   */
  def hasCitizenAt(pos: Position): Boolean = {
    citizenMap.values.find(_ == pos) != None
  }

  /**
   * What is the location of this citizen?
   * // todo: use messaging
   */
  def getPosition(c: Citizen): Option[Position] = {
    citizenMap.get(c)
  }

  /**
   * Sends a message to every SimulationActor on the specified position
   * and returns futures to their answers
   * // todo: use messaging? 
   */
  def send(pos: Position, m: Message): Set[Future[Any]] = {
    (citizenMap.filter(_._2 == pos).map(_._1 !! m) ++ structureMap.filter(_._2 == pos).map(_._1 !! m)).toSet
  }

  /**
   *  Moves a citizen to the specified position.
   */
  private def moveCitizen(c: Citizen, target: SimulationActor) {
    log.info(c + " is moving towards " + target)
    val pos = structureMap(target)
    val path = map.getPath(citizenMap(c), structureMap(target)).get

    citizenMovement(c) = path

    log.fine(c + " is now moving along " + path)
  }

  /**
   * Updates the position of all moving citizens.
   */
  private def updateMovement {
    citizenMovement foreach {
      entry =>
        val citizen = entry._1
        val path = entry._2

        citizenMap(citizen) = path.head
        if (path.tail.isEmpty) {
          citizenMovement.remove(citizen)
          log.fine(citizen + " arrived at " + path.head)
          citizen ! Arrived(map(path.head).get.structure.get)
        }
        else {
          citizenMovement(citizen) = path.tail
        }
    }
  }

  /**
   * Returns the actor that satisfied p and is closest to a.  
   */
  private def findClosest(a: SimulationActor, p: SimulationActor => Boolean): Option[SimulationActor] = {
    // todo: add distance evaluation
    structureMap.keys.find(p)
  }

}